Khám phá xác thực biểu mẫu mạnh mẽ, hiện đại trong React. Hướng dẫn này đi sâu vào hook experimental_useFormStatus, server actions và mô hình xác thực trạng thái.
Nắm vững xác thực biểu mẫu với `experimental_useFormStatus` của React
Biểu mẫu là nền tảng của tương tác web. Từ việc đăng ký nhận bản tin đơn giản đến một ứng dụng tài chính nhiều bước phức tạp, chúng là kênh chính để người dùng giao tiếp với các ứng dụng của chúng ta. Tuy nhiên, trong nhiều năm, việc quản lý trạng thái biểu mẫu trong React đã là một nguồn gốc của sự phức tạp, boilerplate và sự mệt mỏi về dependency. Chúng ta đã phải vật lộn với các controlled component, chiến đấu với các thư viện quản lý trạng thái và viết vô số hàm `onChange`, tất cả chỉ để theo đuổi trải nghiệm người dùng liền mạch và trực quan.
Đội ngũ React đã và đang xem xét lại khía cạnh cơ bản này của phát triển web, dẫn đến việc giới thiệu một mô hình mới, mạnh mẽ tập trung vào React Server Actions. Mô hình mới này, được xây dựng trên các nguyên tắc nâng cao lũy tiến (progressive enhancement), nhằm mục đích đơn giản hóa việc xử lý biểu mẫu bằng cách di chuyển logic gần hơn với nơi nó thuộc về—thường là máy chủ. Trọng tâm của cuộc cách mạng phía client này là hai hook thử nghiệm mới: `useFormState` và ngôi sao của cuộc thảo luận hôm nay, `experimental_useFormStatus`.
Hướng dẫn toàn diện này sẽ đưa bạn đi sâu vào hook `experimental_useFormStatus`. Chúng ta sẽ không chỉ nhìn vào cú pháp của nó; chúng ta sẽ khám phá mô hình tư duy mà nó kích hoạt: Logic xác thực dựa trên trạng thái (Status-Based Validation Logic). Bạn sẽ học cách hook này tách biệt UI khỏi trạng thái biểu mẫu, đơn giản hóa việc quản lý các trạng thái đang chờ xử lý và hoạt động phối hợp với Server Actions để tạo ra các biểu mẫu mạnh mẽ, dễ tiếp cận và có hiệu suất cao, hoạt động ngay cả trước khi JavaScript tải. Hãy sẵn sàng suy nghĩ lại mọi thứ bạn đã từng biết về việc xây dựng biểu mẫu trong React.
Một sự thay đổi mô hình: Sự tiến hóa của biểu mẫu React
Để đánh giá đầy đủ sự đổi mới mà `useFormStatus` mang lại, trước tiên chúng ta phải hiểu hành trình quản lý biểu mẫu trong hệ sinh thái React. Ngữ cảnh này làm nổi bật những vấn đề mà cách tiếp cận mới này giải quyết một cách tinh tế.
Người cũ: Controlled Components và thư viện bên thứ ba
Trong nhiều năm, cách tiếp cận tiêu chuẩn đối với các biểu mẫu trong React là mẫu controlled component. Điều này bao gồm:
- Sử dụng một biến trạng thái React (ví dụ, từ `useState`) để lưu trữ giá trị của mỗi đầu vào biểu mẫu.
- Viết một trình xử lý `onChange` để cập nhật trạng thái trên mỗi lần gõ phím.
- Truyền biến trạng thái trở lại thuộc tính `value` của đầu vào.
Mặc dù điều này cho phép React kiểm soát hoàn toàn trạng thái của biểu mẫu, nhưng nó lại tạo ra boilerplate đáng kể. Đối với một biểu mẫu có mười trường, bạn có thể cần mười biến trạng thái và mười hàm xử lý. Việc quản lý xác thực, trạng thái lỗi và trạng thái gửi còn phức tạp hơn, thường dẫn đến việc các nhà phát triển tạo ra các custom hook phức tạp hoặc tìm đến các thư viện bên thứ ba toàn diện.
Các thư viện như Formik và React Hook Form nổi lên nhờ khả năng trừu tượng hóa sự phức tạp này. Chúng cung cấp các giải pháp tuyệt vời cho việc quản lý trạng thái, xác thực và tối ưu hóa hiệu suất. Tuy nhiên, chúng đại diện cho một dependency khác để quản lý và thường hoạt động hoàn toàn ở phía client, điều này có thể dẫn đến logic xác thực bị trùng lặp giữa frontend và backend.
Kỷ nguyên mới: Nâng cao lũy tiến và Server Actions
React Server Actions giới thiệu một sự thay đổi mô hình. Ý tưởng cốt lõi là xây dựng trên nền tảng của nền tảng web: phần tử HTML tiêu chuẩn `
Một ví dụ đơn giản: Nút gửi thông minh
Hãy cùng xem trường hợp sử dụng phổ biến nhất. Thay vì một nút `
Tệp: SubmitButton.js
import { experimental_useFormStatus as useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
);
}
Tệp: SignUpForm.js
import { SubmitButton } from './SubmitButton';
import { signUpAction } from './actions'; // Một server action
export function SignUpForm() {
return (
Trong ví dụ này, `SubmitButton` hoàn toàn tự chứa. Nó không nhận bất kỳ prop nào. Nó sử dụng `useFormStatus` để biết khi nào `SignUpForm` đang chờ xử lý và tự động vô hiệu hóa nó và thay đổi văn bản của nó. Đây là một mẫu mạnh mẽ để tách biệt và tạo ra các component có thể tái sử dụng, nhận biết biểu mẫu.
Trọng tâm vấn đề: Logic xác thực dựa trên trạng thái
Bây giờ chúng ta đến với khái niệm cốt lõi. `useFormStatus` không chỉ dành cho các trạng thái tải; nó là một yếu tố quan trọng cho phép một cách suy nghĩ khác về xác thực.
Định nghĩa "Xác thực trạng thái"
Xác thực dựa trên trạng thái (Status-Based Validation) là một mẫu mà phản hồi xác thực chủ yếu được cung cấp cho người dùng để đáp lại nỗ lực gửi biểu mẫu. Thay vì xác thực trên mỗi lần gõ phím (`onChange`) hoặc khi người dùng rời khỏi một trường (`onBlur`), logic xác thực chính chạy khi người dùng gửi biểu mẫu. Kết quả của việc gửi này—*trạng thái* của nó (ví dụ: thành công, lỗi xác thực, lỗi máy chủ)—sau đó được sử dụng để cập nhật UI.
Cách tiếp cận này hoàn toàn phù hợp với React Server Actions. Server action trở thành nguồn gốc sự thật duy nhất cho xác thực. Nó nhận dữ liệu biểu mẫu, xác thực nó dựa trên các quy tắc nghiệp vụ của bạn (ví dụ: "email này đã được sử dụng chưa?"), và trả về một đối tượng trạng thái có cấu trúc cho biết kết quả.
Vai trò của đối tác của nó: `experimental_useFormState`
`useFormStatus` cho chúng ta biết *điều gì* đang xảy ra (đang chờ xử lý), nhưng nó không cho chúng ta biết *kết quả* của điều đã xảy ra. Đối với điều đó, chúng ta cần hook chị em của nó: `experimental_useFormState`.
`useFormState` là một hook được thiết kế để cập nhật trạng thái dựa trên kết quả của một form action. Nó nhận hàm action và một trạng thái ban đầu làm đối số và trả về một trạng thái mới cùng một hàm action được bọc để truyền cho biểu mẫu của bạn.
const [state, formAction] = useFormState(myAction, initialState);
- `state`: Điều này sẽ chứa giá trị trả về từ lần thực thi cuối cùng của `myAction`. Đây là nơi chúng ta sẽ nhận được các thông báo lỗi của mình.
- `formAction`: Đây là một phiên bản mới của action mà bạn nên truyền cho prop `action` của `
`. Khi được gọi, nó sẽ kích hoạt action gốc và cập nhật `state`.
Quy trình kết hợp: Từ nhấp chuột đến phản hồi
Đây là cách `useFormState` và `useFormStatus` hoạt động cùng nhau để tạo ra một vòng lặp xác thực đầy đủ:
- Render ban đầu: Biểu mẫu render với trạng thái ban đầu được cung cấp bởi `useFormState`. Không có lỗi nào được hiển thị.
- Người dùng gửi: Người dùng nhấp vào nút gửi.
- Trạng thái đang chờ xử lý: Hook `useFormStatus` trong nút gửi ngay lập tức báo cáo `pending: true`. Nút bị vô hiệu hóa và hiển thị thông báo tải.
- Thực thi Action: Server action (được bọc bởi `useFormState`) được thực thi với dữ liệu biểu mẫu. Nó thực hiện xác thực.
- Action trả về: Action không xác thực thành công và trả về một đối tượng trạng thái, ví dụ:
`{ message: "Xác thực thất bại", errors: { email: "Email này đã được sử dụng." } }` - Cập nhật trạng thái: `useFormState` nhận giá trị trả về này và cập nhật biến `state` của nó. Điều này kích hoạt việc render lại component biểu mẫu.
- Phản hồi UI: Biểu mẫu render lại. Trạng thái `pending` từ `useFormStatus` trở thành `false`. Component giờ đây có thể đọc `state.errors.email` và hiển thị thông báo lỗi bên cạnh trường nhập email.
Toàn bộ luồng này cung cấp phản hồi rõ ràng, có thẩm quyền từ máy chủ cho người dùng, được thúc đẩy hoàn toàn bởi trạng thái gửi và kết quả.
Thực hành chuyên sâu: Xây dựng biểu mẫu đăng ký nhiều trường
Hãy củng cố các khái niệm này bằng cách xây dựng một biểu mẫu đăng ký hoàn chỉnh, theo phong cách sản xuất. Chúng ta sẽ sử dụng server action để xác thực và cả `useFormState` và `useFormStatus` để tạo ra trải nghiệm người dùng tuyệt vời.
Bước 1: Định nghĩa Server Action với xác thực
Đầu tiên, chúng ta cần server action của mình. Để xác thực mạnh mẽ, chúng ta sẽ sử dụng thư viện Zod phổ biến. Action này sẽ nằm trong một tệp riêng biệt, được đánh dấu bằng chỉ thị `'use server';` nếu bạn đang sử dụng một framework như Next.js.
Tệp: actions/authActions.js
'use server';
import { z } from 'zod';
// Định nghĩa schema xác thực
const registerSchema = z.object({
username: z.string().min(3, 'Tên người dùng phải có ít nhất 3 ký tự.'),
email: z.string().email('Vui lòng nhập địa chỉ email hợp lệ.'),
password: z.string().min(8, 'Mật khẩu phải có ít nhất 8 ký tự.'),
});
// Định nghĩa trạng thái ban đầu cho biểu mẫu của chúng ta
export const initialState = {
message: '',
errors: {},
};
export async function registerUser(prevState, formData) {
// 1. Xác thực dữ liệu biểu mẫu
const validatedFields = registerSchema.safeParse(
Object.fromEntries(formData.entries())
);
// 2. Nếu xác thực thất bại, trả về các lỗi
if (!validatedFields.success) {
return {
message: 'Xác thực thất bại. Vui lòng kiểm tra các trường.',
errors: validatedFields.error.flatten().fieldErrors,
};
}
// 3. (Mô phỏng) Kiểm tra xem người dùng đã tồn tại trong cơ sở dữ liệu chưa
// Trong một ứng dụng thực tế, bạn sẽ truy vấn cơ sở dữ liệu của mình ở đây.
if (validatedFields.data.email === 'user@example.com') {
return {
message: 'Đăng ký thất bại.',
errors: { email: ['Email này đã được đăng ký.'] },
};
}
// 4. (Mô phỏng) Tạo người dùng
console.log('Đang tạo người dùng:', validatedFields.data);
// 5. Trả về trạng thái thành công
// Trong một ứng dụng thực tế, bạn có thể chuyển hướng ở đây bằng cách sử dụng `redirect()` từ 'next/navigation'
return {
message: 'Người dùng đã đăng ký thành công!',
errors: {},
};
}
Server action này là bộ não của biểu mẫu của chúng ta. Nó tự chứa, an toàn và cung cấp cấu trúc dữ liệu rõ ràng cho cả trạng thái thành công và lỗi.
Bước 2: Xây dựng các Component có thể tái sử dụng, nhận biết trạng thái
Để giữ cho component biểu mẫu chính của chúng ta sạch sẽ, chúng ta sẽ tạo các component chuyên dụng cho các input và nút gửi của mình.
Tệp: components/SubmitButton.js
'use client';
import { experimental_useFormStatus as useFormStatus } from 'react-dom';
export function SubmitButton({ label }) {
const { pending } = useFormStatus();
return (
);
}
Lưu ý việc sử dụng `aria-disabled={pending}`. Đây là một thực hành truy cập quan trọng, đảm bảo trình đọc màn hình thông báo trạng thái bị vô hiệu hóa một cách chính xác.
Bước 3: Lắp ráp biểu mẫu chính với `useFormState`
Bây giờ, hãy tổng hợp mọi thứ trong component biểu mẫu chính của chúng ta. Chúng ta sẽ sử dụng `useFormState` để kết nối UI của chúng ta với action `registerUser`.
Tệp: components/RegistrationForm.js
{state.message} {state.message}
{state.errors.username[0]}
{state.errors.email[0]}
{state.errors.password[0]}
'use client';
import { experimental_useFormState as useFormState } from 'react-dom';
import { registerUser, initialState } from '../actions/authActions';
import { SubmitButton } from './SubmitButton';
export function RegistrationForm() {
const [state, formAction] = useFormState(registerUser, initialState);
return (
Đăng ký
{state?.message && !state.errors &&
Component này bây giờ là khai báo và sạch sẽ. Nó không quản lý bất kỳ trạng thái nào của riêng nó, ngoài đối tượng `state` được cung cấp bởi `useFormState`. Công việc duy nhất của nó là render UI dựa trên trạng thái đó. Logic để vô hiệu hóa nút được đóng gói trong `SubmitButton`, và tất cả logic xác thực nằm trong `authActions.js`. Sự tách biệt các mối quan tâm này là một chiến thắng lớn cho khả năng bảo trì.
Kỹ thuật nâng cao và các phương pháp hay nhất chuyên nghiệp
Mặc dù mẫu cơ bản mạnh mẽ, các ứng dụng trong thế giới thực thường yêu cầu nhiều sắc thái hơn. Hãy khám phá một số kỹ thuật nâng cao.
Cách tiếp cận kết hợp: Kết hợp xác thực tức thì và sau khi gửi
Xác thực dựa trên trạng thái rất tốt cho các kiểm tra phía máy chủ, nhưng việc chờ đợi một vòng lặp mạng để thông báo cho người dùng rằng email của họ không hợp lệ có thể chậm. Một cách tiếp cận kết hợp thường là tốt nhất:
- Sử dụng xác thực HTML5: Đừng quên những điều cơ bản! Các thuộc tính như `required`, `type="email"`, `minLength` và `pattern` cung cấp phản hồi tức thì, nguyên bản của trình duyệt mà không tốn chi phí.
- Xác thực phía client nhẹ: Đối với các kiểm tra hoàn toàn về mặt hình thức hoặc định dạng (ví dụ: chỉ báo độ mạnh mật khẩu), bạn vẫn có thể sử dụng một lượng nhỏ `useState` và các trình xử lý `onChange`.
- Thẩm quyền phía máy chủ: Dành server action cho các xác thực logic nghiệp vụ quan trọng nhất mà không thể thực hiện ở phía client (ví dụ: kiểm tra tên người dùng duy nhất, xác thực chống lại các bản ghi cơ sở dữ liệu).
Điều này mang lại cho bạn những điều tốt nhất của cả hai thế giới: phản hồi tức thì cho các lỗi đơn giản và xác thực có thẩm quyền cho các quy tắc phức tạp.
Khả năng tiếp cận (A11y): Xây dựng biểu mẫu cho mọi người
Khả năng tiếp cận là không thể bỏ qua. Khi triển khai xác thực dựa trên trạng thái, hãy ghi nhớ những điểm sau:
- Thông báo lỗi: Trong ví dụ của chúng ta, chúng ta đã sử dụng `aria-live="polite"` trên các container thông báo lỗi. Điều này cho trình đọc màn hình biết để thông báo lỗi ngay khi nó xuất hiện, mà không làm gián đoạn luồng hiện tại của người dùng.
- Liên kết lỗi với các Input: Để kết nối mạnh mẽ hơn, hãy sử dụng thuộc tính `aria-describedby`. Input có thể trỏ đến ID của container thông báo lỗi của nó, tạo một liên kết có lập trình.
- Quản lý tiêu điểm (Focus Management): Sau khi gửi có lỗi, hãy xem xét việc di chuyển tiêu điểm một cách lập trình đến trường không hợp lệ đầu tiên. Điều này giúp người dùng không phải tìm kiếm lỗi đã xảy ra.
Optimistic UI với thuộc tính `data` của `useFormStatus`
Hãy tưởng tượng một ứng dụng mạng xã hội nơi người dùng đăng một bình luận. Thay vì hiển thị một vòng quay trong một giây, bạn có thể làm cho ứng dụng có cảm giác tức thì. Thuộc tính `data` từ `useFormStatus` hoàn hảo cho điều này.
Khi biểu mẫu được gửi, `pending` trở thành true và `data` được điền với `FormData` của lần gửi. Bạn có thể ngay lập tức render bình luận mới ở trạng thái trực quan tạm thời, 'đang chờ xử lý' bằng cách sử dụng `data` này. Nếu server action thành công, bạn thay thế bình luận đang chờ xử lý bằng dữ liệu cuối cùng từ máy chủ. Nếu nó thất bại, bạn có thể xóa bình luận đang chờ xử lý và hiển thị lỗi. Điều này làm cho ứng dụng có cảm giác phản hồi cực kỳ nhanh.
Điều hướng vùng nước "thử nghiệm"
Điều quan trọng là phải đề cập đến tiền tố "experimental" trong `experimental_useFormStatus` và `experimental_useFormState`.
"Experimental" thực sự có nghĩa là gì
Khi React gắn nhãn một API là thử nghiệm, điều đó có nghĩa là:
- API có thể thay đổi: Tên, đối số hoặc giá trị trả về có thể bị thay đổi trong một bản phát hành React tương lai mà không tuân theo quy ước semantic versioning (SemVer) tiêu chuẩn cho các thay đổi gây lỗi (breaking changes).
- Có thể có lỗi: Là một tính năng mới, nó có thể có các trường hợp ngoại lệ (edge cases) chưa được hiểu đầy đủ hoặc giải quyết.
- Tài liệu có thể còn ít: Mặc dù các khái niệm cốt lõi đã được ghi lại, các hướng dẫn chi tiết về các mẫu nâng cao có thể vẫn đang phát triển.
Khi nào nên áp dụng và khi nào nên đợi
Vậy, bạn có nên sử dụng nó trong dự án của mình không? Câu trả lời phụ thuộc vào ngữ cảnh của bạn:
- Tốt cho: Các dự án cá nhân, công cụ nội bộ, các công ty khởi nghiệp hoặc các nhóm thoải mái với việc quản lý các thay đổi API tiềm năng. Việc sử dụng nó trong một framework như Next.js (đã tích hợp các tính năng này vào App Router của nó) thường là một lựa chọn an toàn hơn, vì framework có thể giúp trừu tượng hóa một số sự thay đổi.
- Sử dụng cẩn thận cho: Các ứng dụng doanh nghiệp quy mô lớn, các hệ thống quan trọng hoặc các dự án có hợp đồng bảo trì dài hạn mà sự ổn định API là tối quan trọng. Trong những trường hợp này, có thể nên đợi cho đến khi các hook được nâng cấp thành một API ổn định.
Luôn theo dõi blog và tài liệu chính thức của React để biết các thông báo liên quan đến việc ổn định các hook này.
Kết luận: Tương lai của biểu mẫu trong React
Sự ra đời của `experimental_useFormStatus` và các API liên quan của nó không chỉ là một công cụ mới; nó đại diện cho một sự thay đổi triết lý trong cách chúng ta xây dựng trải nghiệm tương tác với React. Bằng cách chấp nhận các nền tảng của nền tảng web và đặt logic trạng thái trên máy chủ, chúng ta có thể xây dựng các ứng dụng đơn giản hơn, linh hoạt hơn và thường có hiệu suất cao hơn.
Chúng ta đã thấy cách `useFormStatus` cung cấp một cách sạch sẽ, tách rời để các component phản ứng với vòng đời của việc gửi biểu mẫu. Nó loại bỏ prop drilling cho các trạng thái đang chờ xử lý và cho phép các component UI tự chứa, thanh lịch như một `SubmitButton` thông minh. Khi kết hợp với `useFormState`, nó mở khóa mẫu xác thực dựa trên trạng thái mạnh mẽ, nơi máy chủ là thẩm quyền cuối cùng, và trách nhiệm chính của client là render trạng thái được trả về bởi server action.
Mặc dù thẻ "experimental" đòi hỏi một mức độ thận trọng, hướng đi là rõ ràng. Tương lai của các biểu mẫu trong React là một trong những nâng cao lũy tiến, quản lý trạng thái đơn giản hóa và sự tích hợp mạnh mẽ, liền mạch giữa logic client và server. Bằng cách nắm vững các hook mới này ngay hôm nay, bạn không chỉ học một API mới; bạn đang chuẩn bị cho thế hệ phát triển ứng dụng web tiếp theo với React.